package org.crud4j.core;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.jexl.Expression;
import org.apache.commons.jexl.ExpressionFactory;
import org.apache.commons.jexl.JexlContext;
import org.apache.commons.jexl.JexlHelper;
import org.apache.log4j.Logger;
import org.crud4j.core.annotation.CrudInstanceLabel;
import org.crud4j.core.defprovider.CrudBeanDefFactory;

/**
 * Abstract class with common functionality to any CrudService
 */
public abstract class AbstractCrudService implements CrudService {

	private static String datePattern = "yyyy/MM/dd HH:mm:ss";

	/**
	 * logger
	 */
	private static Logger log = Logger.getLogger(AbstractCrudService.class);

	/**
	 * {@inheritDoc}
	 */
	public CrudBean create(Class type) {
		log.debug("create crud bean: " + type.getName());
		CrudBeanDef beanDef = CrudBeanDefFactory.getDefinition(type);
		CrudBean bean = new CrudBean();
		bean.setDef(beanDef);

		Map<String, CrudField> fields = new HashMap<String, CrudField>();
		for (CrudFieldDef fieldDef : beanDef.getFieldDefs()) {
			CrudField field = new CrudField();
			field.setDef(fieldDef);
			fields.put(fieldDef.getName(), field);
		}

		bean.setFieldsMap(fields);

		return bean;
	}

	/**
	 * {@inheritdoc}
	 */
	public CrudBrowsePage findAll(Class type) {
		log.debug("find all " + type.getName());
		return this.browse(type, Integer.MAX_VALUE, 0);
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public CrudBrowsePage browse(Class type, Integer pageSize, Integer page) {
		log.debug("browse: " + type.getName() + ". page size: " + pageSize
				+ ". page: " + page);
		return this.filteredBrowse(type, pageSize, page, Collections.EMPTY_MAP);
	}

	/**
	 * Populates a crud bean from the given business bean
	 * 
	 * @param bean
	 *            the business bean
	 * @param cBean
	 *            the crud bean
	 * @param loadFk
	 *            flag that indicates whether the fk fields should be loaded
	 */
	protected void populateCRUDBean(Object bean, CrudBean cBean, boolean loadFk) {

		cBean.setLabel(this.createLabelForEntity(bean));

		Collection<CrudField> fields = cBean.getFields();
		for (CrudField field : fields) {
			try {
				if (field.getDef().isCrudBean() && loadFk) { // FK
					Object fkBean = PropertyUtils.getProperty(bean, field
							.getDef().getName());
					if (fkBean != null) {
						CrudBean fkCrudBean = this.create(fkBean.getClass());
						this.populateCRUDBean(fkBean, fkCrudBean, false);
						if (fkCrudBean.getIds().size() != 1) {
							throw new UnsupportedOperationException(
									"compound fks are not supported yet");
						}
						field.setValue(fkCrudBean.getIds().iterator().next()
								.getValue());
					}
				} else if (field.getDef().getType().equals(Date.class)) { // FECHA
					Date date = (Date) PropertyUtils.getProperty(bean, field
							.getDef().getName());
					if (date == null) {
						field.setValue("");
					} else {
						DateFormat df = new SimpleDateFormat(datePattern);
						field.setValue(df.format(date));
					}
				} else { // Campos comunes
					field.setValue(BeanUtils.getProperty(bean, field.getDef()
							.getName()));
				}
			} catch (Exception e) {
				throw new RuntimeException(
						"unexpected exception while retrieving value from field "
								+ field.getDef().getName() + " of bean "
								+ bean.getClass().getName(), e);
			}
		}
	}

	/**
	 * Creates a label for the given business bean
	 * 
	 * @param bean
	 *            the business bean
	 * @return the label for the given business bean
	 */
	@SuppressWarnings("unchecked")
	private String createLabelForEntity(Object bean) {
		String label = null;

		Class clazz = bean.getClass();
		CrudInstanceLabel labelAnn = null;

		while (clazz != null && labelAnn == null) {
			if (clazz.isAnnotationPresent(CrudInstanceLabel.class)) {
				labelAnn = (CrudInstanceLabel) clazz
						.getAnnotation(CrudInstanceLabel.class);
				try {
					JexlContext ctx = JexlHelper.createContext();
					ctx.getVars().put("bean", bean);
					Expression e = ExpressionFactory.createExpression(labelAnn
							.value());
					label = e.evaluate(ctx).toString();
				} catch (Exception e) {
					throw new UnableToEvaluateLabelException(labelAnn.value(),
							bean.getClass(), e);
				}
			} else {
				if (!clazz.getSuperclass().equals(Object.class)) {
					clazz = clazz.getSuperclass();
				} else {
					clazz = null;
				}
			}
		}

		return label;
	}

}
